跳到主要内容

Go 的 Request 结构体解析

Request 结构体

Go 通过一个 Request 结构体来表示 HTTP 请求报文,这个结构体位于内置的 net/http 包中,其中包含了 HTTP 请求的所有信息,包括请求 URL、请求头、请求实体、表单信息等,平时常用的、比较重要的一些字段如下所示:

  • URL:请求 URL
  • Method:请求方法
  • Proto:HTTP 协议版本
  • Header:请求头(字典类型的键值对集合)
  • Body:请求实体(实现了 io.ReadCloser 接口的只读类型)
  • Form、PostForm、MultipartForm:请求表单相关字段,可用于存储表单请求信息

另外还有很多其他字段,比如 Host、From、ContentLength 等,这里就不一一列举了

URL 字段

在 Go 语言的 http.Request 对象中,用于表示请求 URL 的 URL 字段是一个 url.URL 类型的指针:

来简单介绍下其中常见的字段:

  • Scheme 表示 HTTP 协议是 HTTPS 还是 HTTP,在上面的例子中是 https;(回环请求是不显示的)
  • 对于一些需要认证才能访问的应用,需要提供 User 信息;
  • Host 字段表示域名/主机信息,如果服务器监听端口不是默认的 80 端口的话,还会加 :端口号
  • Path 表示 HTTP 请求路径,一般应用首页是空字符串,或者 /
  • Query 相关字段表示 URL 中的查询字符串,也就是 URL 中 ? 之后的部分;
  • Fragment 表示 URL 中的锚点信息,也就是 URL 中 # 之后的部分。

注意,如果使用的是 localhost,要取得这个 host 得用 r.HOST 否则 r.URL.Host 可能为空(在本地开发环境中,Host 始终为空)

func index(w http.ResponseWriter, r *http.Request) {
if r.URL.Scheme != "https" {
http.Redirect(w, r, "https://"+r.Host+r.URL.Path, 301)
return
}
//....
}

因此,常见的 URL 完整格式如下:

scheme://[user@]host/path[?query][#fragment]

补充:如果请求是从浏览器发送的话,我们无法获取 URL 中的 Fragment 信息,这不是 Go 的问题,而是浏览器根本没有将其发送到服务端。那为什么还要提供这个字段呢?因为不是所有的请求都是从浏览器发送的,而且 Request 也可以在客户端库中使用。

请求头

请求头和响应头都通过 Header 字段表示,Header 是一个键值对字典,键是字符串,值是字符串切片。Header 提供了增删该查方法用于对请求头进行读取和设置。

要获取某个请求头的值很简单,通过 Header 对象提供的 Get 方法,传入对应的字段名即可,比如要获取请求头中 User-Agent 字段,可以这么做:

r.Header.Get("User-Agent")

此外,我们还可以通过 Header 提供的 Add 方法新增请求头:

r.Header.Add("test", "value1")

通过 Header 提供的 Set 方法修改请求头:

r.Header.Set("test", "value2")

以及通过 Header 提供的 Del 方法删除请求头:

r.Header.Del("test")

请求实体

请求实体和响应实体都通过 Body 字段表示,该字段是 io.ReadCloser 接口类型。顾名思义,这个类型实现了 io.Readerio.Closer 接口。

io.Reader 提供了 Read 方法,用于读取传入的字节切片并返回读取的字节数以及错误信息,io.Closer 提供了 Close 方法,因此,你可以在 Body 上调用 Read 方法读取请求实体的内容,调用 Close 方法释放资源。

对于请求实体来说,对应的 Body 访问路径是 http.Request.Body,下面编写一段测试代码来演示请求实体的读取,在 goblog/handlers/post.go 中新增一个 AddPost 处理器方法:

func AddPost(w http.ResponseWriter, r *http.Request)  {
len := r.ContentLength // 获取请求实体长度
body := make([]byte, len) // 创建存放请求实体的字节切片
r.Body.Read(body) // 调用 Read 方法读取请求实体并将返回内容存放到上面创建的字节切片
io.WriteString(w, string(body)) // 将请求实体作为响应实体返回
}

值得注意,这个 Body 读取后之后,如果想要再次读取,需要把数据塞回去

ioutil.NopCloser(bytes.NewReader([]byte("这是数据")))

读取 Form

Go 也为此提供多个不同的结构体帮助我们读取不同请求类型的数据,首先,我们可以通过请求对象上的 Form 读取所有 GET/POST 请求数据,在 handlers/post.go 中新增 EditPost 方法如下:

func EditPost(w http.ResponseWriter, r *http.Request)  {
r.ParseForm()
fmt.Fprintln(w, r.Form)
}

这个 r.Form 就是一个 Map

fmt.Println(r.Form["username"])

需要注意的是,在通过 r.Form 获取所有请求数据之前,必须要先通过 r.ParseForm() 解析所有请求数据,否则无法获取数据。

也可以通过 Form 提供的 Get 方法,就像我们从一个普通字典类型获取键值一样:

id1 := r.Form["id"]
id2 := r.Form.Get("id")

只不过两者的返回值类型不一样,前者是一个字符串切片,后者是一个字符串值:

[1]
1

读取 PostForm

上面的结果同时返回了查询字符串和请求实体,如果只想获取请求实体(即 POST 表单中的数据),可以通过 PostForm 实现:

func EditPost(w http.ResponseWriter, r *http.Request)  {
r.ParseForm()
id := r.Form.Get("id")
fmt.Println("post id:", id)
fmt.Println("form data:", r.PostForm)
io.WriteString(w, "表单提交成功")
}

通过 PostForm 获取具体参数值的方式和 Form 一样

FormValue/PostFormValue

最后,还可以通过 FormValue 和 PostFormValue 获取用户请求数据,使用它们的好处是 不再需要单独调用 ParseForm 对表单数据进行解析,不过使用这两个方法的时候只能获取特定请求数据,不能一次获取所有请求数据:

func EditPost(w http.ResponseWriter, r *http.Request)  {
fmt.Println("post id:", r.FormValue("id"))
fmt.Println("post title:", r.PostFormValue("title"))
fmt.Println("post title:", r.PostFormValue("content"))
io.WriteString(w, "表单提交成功")
}

FormValue/PostFormValue 的区别和 Form/PostForm 一样,这里通过命名就可以看出来,前者可以获取所有 GET/POST 请求数据(即查询字符串和请求实体),后者只能获取 POST 请求实体数据。

注:FormValue/PostFormValue 之所以不用显式调用 ParseForm 解析请求数据,是因为底层对其进行了封装,实际上还是要调用这个方法。

获取 JSON 请求数据

上面的示例默认都是基于 HTML 表单请求,对于客户端提交的 JSON 格式数据,使用 ParseForm 是无法解析并获取数据的,因为 HTML 表单请求数据默认是通过 application/x-www-form-urlencoded 编码的,而 JSON 请求数据通常是通过 application/json 编码,ParseForm 只能解析通过 application/x-www-form-urlencoded 编码的数据。

json 数据直接使用 io 读取到数据再解析就行了

type Post struct {
Title string `json:"title"`
Content string `json:"content"`
}

func AddPost(w http.ResponseWriter, r *http.Request) {
len := r.ContentLength // 获取请求实体长度
body := make([]byte, len) // 创建存放请求实体的字节切片
r.Body.Read(body) // 调用 Read 方法读取请求实体并将返回内容存放到上面创建的字节切片
// io.WriteString(w, string(body))
post := Post{}
json.Unmarshal(body, &post) // 对读取的 JSON 数据进行解析
fmt.Fprintf(w, "%#v\n", post) // 格式化输出结果
}

MultipartForm 请求

Go 语言为文件类型请求数据提供了单独的请求字段 MultipartForm,它是一个 multipart.Form 类型的指针,要解析并获取这个字段,可以这么做:

func EditPost(w http.ResponseWriter, r *http.Request)  {
...

r.ParseMultipartForm(1024)
fmt.Println("post file:", r.MultipartForm)

io.WriteString(w, "表单提交成功")
}

这里,需要在调用 ParseMultipartForm 时传入存储解析后文件的最大内存值(单位是字节)。MultipartForm 包含了所有 POST 表单请求字段,即 PostForm 中的所有内容,但不包含 URL 查询字符串中的请求参数。

MultipartForm 返回的值包含两个部分,一部分是单纯的 POST 请求字段,我们可以通过 Value 字段来访问它,另一部分就是包含文件信息的字典,我么可以通过 File 字段来访问它。

打开 Postman 模拟客户端请求,填写 URL 和 表单字段(数据编码类型选择 form-data,即 multipart/form-data):

表单数据设置好了之后,勾选上所有数据,然后点击「Send」发送请求,看到响应实体(Body)中显示「表单提交成功」,表明服务端已经处理完请求并成功返回响应,我们到启动 HTTP 服务器的位置查看服务端日志:

可以看到请求头中的 Content-Type 是 multipart/form-data,并且通过 r.MultipartForm 成功获取到了 POST 表单数据,包含文件信息(位于一个独立的 map 中)。